package fatworm.storage.file;

import static fatworm.storage.file.Page.*;

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.*;

/**
 * The SimpleDB file manager. The database system stores its data as files
 * within a specified directory. The file manager provides methods for reading
 * the contents of a file block to a Java byte buffer, writing the contents of a
 * byte buffer to a file block, and appending the contents of a byte buffer to
 * the end of a file. These methods are called exclusively by the class
 * {@link simpledb.file.Page Page}, and are thus package-private. The class also
 * contains two public methods: Method {@link #isNew() isNew} is called during
 * system initialization by {@link simpledb.server.SimpleDB#init}. Method
 * {@link #size(String) size} is called by the log manager and transaction
 * manager to determine the end of the file.
 * 
 * @author Edward Sciore
 */
public class FileMgr
{
    private File rootDir;
    private Map<String, FileChannel> openFiles = new HashMap<String, FileChannel>();

    private void deleteAll(File a)
    {
        if (a.isDirectory())
            for (File b : a.listFiles())
                deleteAll(b);
        a.delete();
    }
    public FileMgr(String rootPath)
    {
        rootDir = new File(rootPath);
        // TODO clear for test
        deleteAll(rootDir);
        if (!rootDir.exists())
            rootDir.mkdirs();
        if (!rootDir.isDirectory())
            throw new RuntimeException("cannot create " + rootPath);
        // remove any leftover temporary tables
        /*for (String filename : dbDir.list())
            if (filename.startsWith("temp"))
                new File(dbDir, filename).delete();*/
    }
    /**
     * Reads the contents of a disk block into a bytebuffer.
     * 
     * @param blk
     *            a reference to a disk block
     * @param bb
     *            the bytebuffer
     */
    synchronized void read(Block blk, ByteBuffer bb)
    {
        try
        {
            bb.clear();
            FileChannel fc = getFile(blk.filename);
            fc.read(bb, blk.blknum * BLOCK_SIZE);
        }
        catch (IOException e)
        {
            throw new RuntimeException("cannot read block " + blk);
        }
    }
    /**
     * Writes the contents of a bytebuffer into a disk block.
     * 
     * @param blk
     *            a reference to a disk block
     * @param bb
     *            the bytebuffer
     */
    synchronized void write(Block blk, ByteBuffer bb)
    {
        try
        {
            bb.rewind();
            FileChannel fc = getFile(blk.filename);
            fc.write(bb, blk.blknum * BLOCK_SIZE);
        }
        catch (IOException e)
        {
            throw new RuntimeException("cannot write block" + blk);
        }
    }
    /**
     * Appends the contents of a bytebuffer to the end of the specified file.
     * 
     * @param filename
     *            the name of the file
     * @param bb
     *            the bytebuffer
     * @return a reference to the newly-created block.
     */
    synchronized Block append(String filename, ByteBuffer bb)
    {
        int newblknum = size(filename);
        Block blk = new Block(filename, newblknum);
        write(blk, bb);
        return blk;
    }
    /**
     * Returns the number of blocks in the specified file.
     * 
     * @param filename
     *            the name of the file
     * @return the number of blocks in the file
     */
    public synchronized int size(String filename)
    {
        try
        {
            FileChannel fc = getFile(filename);
            return (int) (fc.size() / BLOCK_SIZE);
        }
        catch (IOException e)
        {
            throw new RuntimeException("cannot access " + filename);
        }
    }
    /**
     * Returns the file channel for the specified filename. The file channel is
     * stored in a map keyed on the filename. If the file is not open, then it
     * is opened and the file channel is added to the map.
     * 
     * @param filename
     *            the specified filename
     * @return the file channel associated with the open file.
     * @throws IOException
     */
    private FileChannel getFile(String filename) throws IOException
    {
        FileChannel fc = openFiles.get(filename);
        if (fc == null)
        {
            File dbTable = new File(rootDir, filename);
            if (!dbTable.getParentFile().exists())
                dbTable.getParentFile().mkdirs();
            RandomAccessFile f = new RandomAccessFile(dbTable, "rws");
            fc = f.getChannel();
            openFiles.put(filename, fc);
        }
        return fc;
    }
}
